﻿/*
 * https://github.com/BerndK/SvgToXaml
 * 参考 SvgToXaml 项目 ConverterLogic 修改
 */

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using SharpVectors.Converters;
using SharpVectors.Renderers.Wpf;

namespace SvgViewer.Utilities
{
    /// <summary>
    /// Svg 转换辅助类
    /// </summary>
    public static class SvgHelper
    {
        static SvgHelper()
        {
            NsManager.AddNamespace("defns", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
            NsManager.AddNamespace("x",     "http://schemas.microsoft.com/winfx/2006/xaml");
        }

        internal static XNamespace          NsDef     = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
        internal static XmlNamespaceManager NsManager = new(new NameTable());

        /// <summary>
        /// 将Svg图像文件转换为 <see cref="ImageSource"/>
        /// </summary>
        /// <param name="filePath">Svg 文件路径</param>
        /// <returns>返回转换后的 <see cref="ImageSource"/></returns>
        public static ImageSource SvgToImageSource(string filePath)
        {
            var dg = ConvertFileToDrawingGroup(filePath);
            return new DrawingImage(dg);
        }

        /// <summary>
        /// 将 <see cref="ImageSource"/> 转换为 Xaml
        /// </summary>
        /// <param name="imageSource">要转换的 <see cref="ImageSource"/></param>
        /// <returns></returns>
        public static string SvgObjectToXaml(ImageSource imageSource)
        {
            var xamlUntidy = WpfObjToXaml(imageSource);

            var doc = XDocument.Parse(xamlUntidy);
            BeautifyDrawingElement(doc.Root);
            var xamlWithNamespaces = doc.ToString();

            var xamlClean = RemoveNamespaceDeclarations(xamlWithNamespaces);
            return xamlClean;
        }


        private static DrawingGroup ConvertFileToDrawingGroup(string filepath)
        {
            var dg = SvgFileToDrawingGroup(filepath);
            SetSizeToGeometries(dg);
            RemoveObjectNames(dg);
            return dg;
        }

        internal static void SetSizeToGeometries(DrawingGroup dg)
        {
            var size = GetSizeFromDrawingGroup(dg);
            if (size.HasValue)
            {
                var geometries = GetPathGeometries(dg).ToList();
                geometries.ForEach(g => SizeGeometry(g, size.Value));
            }
        }

        private static IEnumerable<PathGeometry> GetPathGeometries(Drawing drawing)
        {
            var result = new List<PathGeometry>();

            void HandleDrawing(Drawing aDrawing)
            {
                if (aDrawing is DrawingGroup group)
                    foreach (var d in group.Children)
                    {
                        HandleDrawing(d);
                    }

                if (aDrawing is GeometryDrawing gd)
                {
                    var geometry = gd.Geometry;
                    if (geometry is PathGeometry pathGeometry)
                    {
                        result.Add(pathGeometry);
                    }
                }
            }

            HandleDrawing(drawing);

            return result;
        }

        private static void SizeGeometry(PathGeometry pg, Size size)
        {
            if (size.Height > 0)
            {
                PathFigure[] sizeFigures = {
                    new(new Point(size.Width, size.Height), Enumerable.Empty<PathSegment>(), true),
                    new(new Point(0,          0), Enumerable.Empty<PathSegment>(), true),
                };

                var newGeo = new PathGeometry(sizeFigures.Concat(pg.Figures), pg.FillRule, null);
                pg.Clear();
                pg.AddGeometry(newGeo);
            }
        }

        internal static DrawingGroup SvgFileToDrawingGroup(string filePath)
        {
            var reader = new FileSvgReader(new WpfDrawingSettings {
                IncludeRuntime = false, TextAsGeometry = false, OptimizePath = true
            });

            filePath = Path.GetFullPath(filePath);
            Stream stream = IsSvgz(filePath) ?
                new GZipStream(File.OpenRead(filePath), CompressionMode.Decompress, false) :
                File.OpenRead(filePath);
            var doc = XDocument.Load(stream);
            stream.Dispose();

            using (var ms = new MemoryStream())
            {
                doc.Save(ms);
                ms.Position = 0;
                reader.Read(ms);
                return reader.Drawing;
            }
        }

        private static bool IsSvgz(string filepath)
        {
            return string.Equals(Path.GetExtension(filepath), ".svgz", StringComparison.OrdinalIgnoreCase);
        }

        private static string WpfObjToXaml(object wpfObject)
        {
            var writer = new XmlXamlWriter(new WpfDrawingSettings {
                IncludeRuntime = false
            });
            var xaml = writer.Save(wpfObject);
            return xaml;
        }

        private static void BeautifyDrawingElement(XElement drawingElement)
        {
            InlineClipping(drawingElement);
            RemoveCascadedDrawingGroup(drawingElement);
            CollapsePathGeometries(drawingElement);
        }

        private static void InlineClipping(XElement drawingElement)
        {
            var clipElement = GetClipElement(drawingElement, out var clipRect);
            if (clipElement != null && clipElement.Parent?.Name.LocalName == "DrawingGroup")
            {
                clipElement.Parent.Add(new XAttribute("ClipGeometry",
                    string.Format(CultureInfo.InvariantCulture, "M{0},{1} V{2} H{3} V{0} H{1} Z", clipRect.Left,
                        clipRect.Top, clipRect.Bottom, clipRect.Right)));
                clipElement.Remove();
            }
        }

        private static void RemoveCascadedDrawingGroup(XElement drawingElement)
        {
            var drawingGroups = drawingElement.DescendantsAndSelf(NsDef + "DrawingGroup");
            foreach (var drawingGroup in drawingGroups)
            {
                var elems = drawingGroup.Elements().ToList();
                if (elems.Count == 1 && elems[0].Name.LocalName == "DrawingGroup")
                {
                    var subGroup     = elems[0];
                    var subAttrNames = subGroup.Attributes().Select(a => a.Name);
                    var attrNames    = drawingGroup.Attributes().Select(a => a.Name);
                    if (subAttrNames.Intersect(attrNames).Any())
                        return;
                    drawingGroup.Add(subGroup.Attributes());
                    drawingGroup.Add(subGroup.Elements());
                    subGroup.Remove();
                }
            }
        }

        private static void CollapsePathGeometries(XElement drawingElement)
        {
            var pathGeometries = drawingElement.Descendants(NsDef + "PathGeometry").ToArray();
            foreach (var pathGeometry in pathGeometries)
            {
                if (pathGeometry.Parent?.Parent               != null &&
                    pathGeometry.Parent.Parent.Name.LocalName == "GeometryDrawing")
                {
                    var attrNames = pathGeometry.Attributes().Select(a => a.Name.LocalName).ToList();
                    if (attrNames.Count <= 2 && attrNames.Contains("Figures") &&
                        (attrNames.Contains("FillRule") || attrNames.Count == 1))
                    {
                        var sFigures     = pathGeometry.Attribute("Figures").Value;
                        var fillRuleAttr = pathGeometry.Attribute("FillRule");
                        if (fillRuleAttr != null)
                        {
                            if (fillRuleAttr.Value == "Nonzero")
                            {
                                sFigures = $"F1 {sFigures}";
                            }
                            else
                            {
                                sFigures = $"F0 {sFigures}";
                            }
                        }

                        pathGeometry.Parent.Parent.Add(new XAttribute("Geometry", sFigures));
                        pathGeometry.Parent.Remove();
                    }
                }
            }
        }


        private static string RemoveNamespaceDeclarations(string xml)
        {
            xml = xml.Replace(" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"", "");
            xml = xml.Replace(" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"",            "");
            return xml;
        }

        private static void RemoveObjectNames(DrawingGroup drawingGroup)
        {
            if (drawingGroup.GetValue(FrameworkElement.NameProperty) != null)
            {
                drawingGroup.SetValue(FrameworkElement.NameProperty, null);
            }

            foreach (var child in drawingGroup.Children.OfType<DependencyObject>())
            {
                if (child.GetValue(FrameworkElement.NameProperty) != null)
                {
                    child.SetValue(FrameworkElement.NameProperty, null);
                }

                if (child is DrawingGroup group)
                {
                    RemoveObjectNames(group);
                }
            }
        }

        private static XElement GetClipElement(XElement drawingGroupElement, out Rect rect)
        {
            rect = default(Rect);
            if (drawingGroupElement == null)
            {
                return null;
            }

            var clipElement = drawingGroupElement.XPathSelectElement(".//defns:DrawingGroup.ClipGeometry", NsManager);
            var rectangleElement = clipElement?.Element(NsDef + "RectangleGeometry");
            var rectAttr = rectangleElement?.Attribute("Rect");
            if (rectAttr != null)
            {
                rect = Rect.Parse(rectAttr.Value);
                return clipElement;
            }

            return null;
        }

        private static Size? GetSizeFromDrawingGroup(DrawingGroup drawingGroup)
        {
            var subGroup = drawingGroup?.Children.OfType<DrawingGroup>().FirstOrDefault(c => c.ClipGeometry != null);
            return subGroup?.ClipGeometry.Bounds.Size;
        }
    }
}